Drag and Drop with Knockout.js
In modern web development, enhancing user interaction and providing intuitive interfaces is crucial. One of the popular methods to achieve this is through drag-and-drop functionality. Knockout.js, a powerful JavaScript library that follows the MVVM (Model-View-ViewModel) pattern, can be seamlessly integrated with drag-and-drop features to create dynamic and interactive web applications. This article will guide you through implementing drag-and-drop functionality in a Knockout.js-based application.
Why Use Knockout.js?
Knockout.js offers a clean way to manage complex data-driven interfaces. It simplifies the process of keeping the UI in sync with underlying data models using declarative bindings. When combined with drag-and-drop, it can significantly enhance the user experience by allowing intuitive interactions with the UI elements.
Setting Up the Environment
Before diving into the implementation, ensure you have the necessary libraries included in your HTML file:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Drag and Drop Demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.min.js"></script>
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<!-- Your HTML content here -->
<script src="script.js"></script>
</body>
</html>
HTML Structure
We will create a basic structure with two sections: one for selecting items and another for dropping the selected items.
<!-- Main container using Bootstrap's grid system -->
<div class="container" data-bind="">
<div>
<!-- Header with main title -->
<h1>
Drag and Drop
</h1>
<hr />
</div>
<!-- Row for drag and drop sections -->
<div class="row g-3">
<!-- Column for selecting items to drag -->
<div class="col-12 col-md-6 h-100" title="Select it">
<p class="fw-semibold">Select It</p>
<div class="droptarget border h-100 w-100 rounded-3 p-2" ondrop="drop(event)" ondragover="allowDrop(event)">
<!-- List of products to drag -->
<ul class="list-group" data-bind="foreach: products">
<li data-bind="attr: { 'data-productid' : id, id : 'dragtarget' + id }" ondragstart="dragStart(event)"
ondragend="dragEnd(event)" draggable="true" class="list-group-item product">
<!-- Controls for adding or removing a product -->
<div class="d-flex float-end position-absolute top-0 end-0">
<span onclick="removeProduct(this)" title="Remove this product"
class="d-none fs-5 pe-2 remove-product text-danger">
<i class="bi bi-dash-circle-fill"></i></span>
<span onclick="addProduct(this)" title="Add this product"
class="fs-5 pe-2 add-product text-success">
<i class="bi bi-plus-circle-fill"></i></span>
</div>
<!-- Product details -->
<div class="d-flex">
<img class="img-fluid" width="100" height="100" data-bind="attr: { src: img, alt: name }" />
<div class="ms-2 w-100 h-100">
<h3 data-bind="text: name"></h3>
<p class="m-0 fw-semibold" data-bind="text: price"></p>
<div class="float-start text-warning fs-4 stars" data-bind="foreach: stars">
<span>★</span> <!-- Unicode character for a filled star -->
</div>
</div>
</div>
</li>
</ul>
</div>
</div>
<!-- Column for dropping selected items -->
<div class="col-12 col-md-6 h-100" title="Drop it">
<div class="d-flex justify-content-between">
<p class="fw-semibold">Drop Here</p>
<p class="fw-semibold">
<span>Total Products - </span>
<span data-bind="text: selectedProducts().length"></span>
</p>
</div>
<!-- Drop area for products -->
<div id="droptarget" class="droptarget border h-100 w-100 rounded-3 p-2 list-group drop-area-element overflow-y-auto"
ondrop="drop(event)" ondragover="allowDrop(event)">
</div>
<!-- Total price display -->
<p class="fw-bold float-end pt-3">
<span>Price - $</span>
<span data-bind="text: totalSelectedProductPrice"></span>
</p>
</div>
</div>
<!-- Footer with additional info -->
<hr />
<div>
<p class="fw-semibold" id="demo"></p>
</div>
</div>
Write needed CSS
Write needed CSS
.droptarget {
width: 100%;
height: 100%;
min-height: 100px;
height: 700px!important;
}
.drop-area-element{
position: relative;
}
.drop-area-element .remove-product {
display: block!important;
cursor: pointer;
}
.add-product {
cursor: pointer;
}
.drop-area-element .add-product{
display: none;
}
JavaScript Logic
Now, let's implement the JavaScript logic to handle the drag-and-drop events and manage the data using Knockout.js.
/* Events fired on the drag target */
function dragStart(event) {
// Store the ID of the dragged element
event.dataTransfer.setData("Text", event.target.id);
// Display a message indicating dragging has started
document.getElementById("demo").innerHTML = "Started dragging";
}
function dragEnd(event) {
// Display a message indicating dragging has ended
document.getElementById("demo").innerHTML = "Finished dragging.";
}
/* Events fired on the drop target */
function allowDrop(event) {
// Prevent the default behavior to allow dropping
event.preventDefault();
}
function drop(event) {
// Prevent the default behavior
event.preventDefault();
// Get the ID of the dragged element
const data = event.dataTransfer.getData("Text");
// Find the element by ID
let ele = document.getElementById(data);
if(ele.dataset && ele.dataset.productid){
// Check if there is space available for the product
if(model.isSpaceAvailableForProduct(ele.dataset.productid)){
// Add the product to the selected products list
model.addSelectedProduct(ele.dataset.productid);
// Append the cloned element to the drop target
event.target.appendChild(ele.cloneNode(true));
} else {
// Alert the user if the maximum limit is reached
window.alert("Maximum 2 products can be added.");
}
}
}
function addProduct(event){
// Get the parent element of the clicked add button
let parent = event.parentNode.parentNode;
if (parent.dataset && parent.dataset.productid) {
// Check if there is space available for the product
if(model.isSpaceAvailableForProduct(parent.dataset.productid)){
// Add the product to the selected products list
model.addSelectedProduct(parent.dataset.productid);
// Append the cloned parent element to the drop target
document.getElementById('droptarget').appendChild(parent.cloneNode(true));
} else {
// Alert the user if the maximum limit is reached
window.alert("Maximum 2 products can be added.");
}
}
}
function removeProduct(event) {
// Get the parent element of the clicked remove button
let parent = event.parentNode.parentNode;
if (parent.dataset && parent.dataset.productid) {
// Remove the product from the selectedProducts array
model.removeSelectedProduct(parent.dataset.productid);
// Remove the parent element from the DOM
parent.parentNode.removeChild(parent);
}
}
// Class representing a Product
class Product {
constructor(productId, name, img, price, stars) {
this.id = productId;
this.name = name;
this.img = img;
this.price = price;
this.stars = ko.observableArray(this.generateStars(stars)); // Generate 5 stars
}
generateStars(count) {
// Generate an array of stars up to the given count
return Array.from({ length: 5 }, (_, i) => i < count).filter(Boolean);
}
}
// ViewModel class for Knockout.js
class ViewModel {
constructor() {
var self = this;
self.name = ko.observable("aksdfhaksdhf");
self.dragMessage = ko.observable("");
self.selectedProductsIds = ko.observableArray([]);
self.selectedProducts = ko.observableArray([]);
// Function to get a product by ID
self.getProductById = function(productId, dataSource) {
return ko.utils.arrayFirst(dataSource(), function(product) {
return product.id === parseInt(productId);
});
};
self.addSelectedProduct = function(productId){
self.selectedProducts.push(self.getProductById(productId, self.products));
};
self.removeSelectedProduct = function(productId) {
var index = self.selectedProducts.indexOf(self.getProductById(productId, self.selectedProducts));
if (index !== -1) {
self.selectedProducts.splice(index, 1);
}
};
self.totalSelectedProductPrice = ko.computed(function() {
var selectedProducts = self.selectedProducts(); // Assuming selectedProducts() returns an array of Product objects
// Use reduce to sum up the prices of all selected products
var totalPrice = selectedProducts.reduce(function(total, product) {
// Parse the price and add it to the total
return total + parseFloat(product.price.replace('$', ''));
}, 0);
// Return the total price rounded to two decimal places
return totalPrice.toFixed(2);
});
self.countFrequencyOfNumber = function(arr, num) {
// Use Array.prototype.reduce to count occurrences of num in arr
return arr.reduce(function(count, element) {
return count + (element.id === parseInt(num) ? 1 : 0);
}, 0);
};
self.isSpaceAvailableForProduct = function(productId) {
// Check if the count of productId is less than 2
return self.countFrequencyOfNumber(self.selectedProducts(), productId) < 2;
};
// Create an array of Product objects
self.products = ko.observableArray([
new Product(1, 'Puma Shoe', 'https://rukminim2.flixcart.com/image/850/1000/l432ikw0/shoe/6/0/l/-original-imagf255et5szt5s.jpeg?q=90&crop=false', '$100.00', 5),
new Product(2, 'Flight Sleeper', 'https://img3.junaroad.com/uiproducts/20423620/pri_175_p-1697706717.jpg', '$20.00', 4),
new Product(3, 'Bucket', 'https://nutristar.co.in/cdn/shop/products/1_488b7df8-acf2-409c-8faa-21ae2bb08001_1024x1024.jpg?v=1579738972', '$30.00', 2),
new Product(4, 'Socks', 'https://m.media-amazon.com/images/I/91mNKVcE0WL._AC_UY1100_.jpg', '$5.00', 3),
new Product(5, 'Pen', 'https://5.imimg.com/data5/SELLER/Default/2020/10/PX/KF/AW/20193325/ink-pen.jpg', '$50.00', 4)
]);
}
}
// Instantiate ViewModel and apply bindings
var model = new ViewModel();
ko.applyBindings(model);
Explanation
HTML Structure:
- Drag Source: A list of products that can be dragged.
- Drop Target: An area where the products can be dropped.
JavaScript Logic:
- Drag-and-Drop Handlers: Functions to handle dragStart, dragEnd, allowDrop, and drop events.
- Product Class: Represents a product with properties and a method to generate star ratings.
- ViewModel Class: Manages the application's state, including the list of products, selected products, and various helper functions.
Conclusion
By integrating Knockout.js with drag-and-drop functionality, you can create a more interactive and engaging user interface. This guide has provided a foundational understanding of implementing these features, allowing you to enhance your web applications significantly. With further customization and optimization, the possibilities are endless. Happy coding!
Leave Comment